package slimarchive

import (
	"bytes"
	"encoding/binary"

	// gethtypes "github.com/ethereum/go-ethereum/core/types"
	// gethrlp "github.com/ethereum/go-ethereum/rlp"
	"github.com/holiman/uint256"
	"github.com/ledgerwatch/erigon-lib/common"
	libcommon "github.com/ledgerwatch/erigon-lib/common"
	"github.com/ledgerwatch/erigon/core/types/accounts"
	"github.com/ledgerwatch/erigon/crypto"
	"github.com/ledgerwatch/erigon/ethdb/pebble"
	"github.com/ledgerwatch/erigon/rlp"
)

var emptyCodeHash = crypto.Keccak256(nil)

const (
	accountType byte = 'A'
	codeType    byte = 'C'
	emptyType   byte = 'E'
	stateType   byte = 'S'
)

type SlimReader struct {
	blockNumber uint64
	txnIndex    uint16
	suffix      []byte

	db          *pebble.Database
	lastSuicide map[libcommon.Address][]byte
	codes       map[libcommon.Hash][]byte
}

func NewSlimReader(blockNumber uint64, txnIndex uint16, db *pebble.Database) *SlimReader {
	r := &SlimReader{
		blockNumber: blockNumber,
		txnIndex:    txnIndex,
		suffix:      make([]byte, 8),
		db:          db,
		lastSuicide: make(map[libcommon.Address][]byte),
		codes:       make(map[libcommon.Hash][]byte),
	}
	binary.BigEndian.PutUint64(r.suffix, blockNumber<<16|uint64(txnIndex))
	return r
}

func (r *SlimReader) ReadAccountData(address common.Address) (*accounts.Account, error) {
	prefix := r.jointKey([]byte{accountType}, address.Bytes())

	keys, vals, err := r.db.Scan(prefix, r.jointKey(prefix, r.suffix), 1, true)
	if err != nil {
		return nil, err
	}
	if len(keys) > 0 {
		if len(vals[0]) == 0 {
			return nil, nil
		}
		acc, err := r.decodeAccount(vals[0])
		if err != nil {
			return nil, err
		}
		balance := new(uint256.Int).SetBytes(acc.Balance.Bytes())
		return &accounts.Account{
			Initialised: true,
			Nonce:       acc.Nonce,
			Balance:     *balance,
			CodeHash:    libcommon.BytesToHash(acc.CodeHash),
		}, nil
	}

	return nil, nil
}

func (r *SlimReader) ReadAccountStorage(address common.Address, incarnation uint64, key *common.Hash) ([]byte, error) {
	prefix := r.jointKey([]byte{stateType}, address.Bytes(), key.Bytes())
	keys, vals, err := r.db.Scan(prefix, r.jointKey(prefix, r.suffix), 1, true)
	if err != nil {
		return nil, err
	}
	if len(keys) > 0 {
		if writeAt, suicideAt := r.extractPostfix(keys[0]), r.tryGetEmpty(address); bytes.Compare(writeAt, suicideAt) > 0 {
			_, content, _, err := rlp.Split(vals[0])
			return content, err
		} else {
			return common.Hash{}.Bytes(), nil
		}
	}
	return nil, nil
}

func (r *SlimReader) ReadAccountCode(address common.Address, incarnation uint64, codeHash common.Hash) ([]byte, error) {
	if bytes.Equal(codeHash[:], emptyCodeHash) {
		return nil, nil
	}
	if code, ok := r.codes[codeHash]; ok {
		return code, nil
	}
	code, err := r.db.Get(r.jointKey([]byte{codeType}, codeHash.Bytes()))
	if err != nil {
		return nil, err
	}
	r.codes[codeHash] = code
	return code, nil
}

func (r *SlimReader) ReadAccountCodeSize(address common.Address, incarnation uint64, codeHash common.Hash) (int, error) {
	if bytes.Equal(codeHash[:], emptyCodeHash) {
		return 0, nil
	}
	code, err := r.ReadAccountCode(address, 0, codeHash)
	return len(code), err
}

func (r *SlimReader) ReadAccountIncarnation(address common.Address) (uint64, error) {
	return 0, nil
}

func (r *SlimReader) jointKey(prefixes ...[]byte) []byte {
	return bytes.Join(prefixes, []byte{})
}

func (r *SlimReader) decodeAccount(data []byte) (*StateAccount, error) {
	if len(data) == 0 {
		return nil, nil
	}
	var account = new(StateAccount)
	err := rlp.DecodeBytes(data, account)
	return account, err
}
func (r *SlimReader) extractPostfix(key []byte) []byte {
	return key[len(key)-8:]
}

func (r *SlimReader) tryGetEmpty(addr common.Address) []byte {
	if r.lastSuicide[addr] != nil {
		return r.lastSuicide[addr]
	}
	prefix := r.jointKey([]byte{emptyType}, addr[:])
	keys, _, err := r.db.Scan(prefix, r.jointKey(prefix, r.suffix), 1, true)
	if err != nil {
		panic("db err when get empty")
	}
	if len(keys) > 0 {
		r.lastSuicide[addr] = r.extractPostfix(keys[0])
	} else {
		r.lastSuicide[addr] = make([]byte, 8)
	}
	return r.lastSuicide[addr]
}
